/*
 * Project CHEN Rendering Engine : /interaction.hpp
 * Created: 2020-12-16, Last modified:  2021-02-19
 * 
 * This is the header file of interaction.cpp
 * 
 * Copyright (C) 2021 Hineven (hineven@pku.edu.cn)
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#ifndef _H_INTERACTION
#define _H_INTERACTION
#include "heads.hpp"
#include "optics.hpp"
#include "geometry.hpp"
#include "flags.hpp"
#include "optics.hpp"
#include "buffer.hpp"
#include "bsdf.hpp"
#include "shape.hpp"
namespace chen {
class Interact {
public:
    // Position in world space
	Point3 p;
    // Floating error bound of p
    Vector3 p_err;
    // Incoming ray direction w_hit
    Vector3 wh;
    // Interact surface / ray normal
    Normal ng;
    // The object causes the interaction
    const Object * hit;
    // Should sides flip
    bool flipped;
    inline Interact () = default;
    Interact (const Point3 & p, const Vector3 & p_err,
        const Vector3 & wh, const Normal & ng, const Object * hit, bool fl):
        p(p), p_err(p_err), wh(wh), ng(ng), hit(hit), flipped(fl) {}

    // Spawn a ray on the side f of the surface according to
    // the standard surface normal.
    inline Ray spawnRay (const Vector3 & d, bool f) const {
        assert(eequal(d.lengthSquared(), 1));
        if(!f) return Ray(offsetOrigin(p, p_err, ng), d);
        else return Ray(offsetOrigin(p, p_err, -ng), d);
    }
    // Spawn a ray on the side f of the surface according to
    // the standard surface normal.
    // With t always equals to 1, this is convenient spawning 
    // shadow rays.
    inline Ray spawnRayTo (const Point3 & d, bool f) const {
        Point3 o;
        if(!f) o = offsetOrigin(p, p_err, ng);
        else o = offsetOrigin(p, p_err, -ng);
        return Ray(o, d-o, 1);
    }
    // Automatically spawns a ray that will not intersect the
    // current hit point.
    inline Ray spawnRayPickSide (const Vector3 & d) const {
        if(dot((Vector3)ng, d) > 0) return Ray(offsetOrigin(p, p_err, ng), d); 
        else return Ray(offsetOrigin(p, p_err, -ng), d);
    }
    // Automatically spawns a ray that will not intersect the
    // current geometry at the current hit point.
    // Note that the t value of the ray will always be 1
    inline Ray spawnRayToPickSide (const Point3 & d) const {
        Point3 o;
        if(dot((Vector3)ng, d-o) < 0) o = offsetOrigin(p, p_err, -ng);
        else o = offsetOrigin(p, p_err, ng);
        return Ray(o, d-o, 1);
    }
    // Get a point which is outside the error bounds of the hit
    // point on a given side (with respect to the standard normal).
    inline Point3 getOutside (bool f) const {
        if(!f) return offsetOrigin(p, p_err, ng);
        else return offsetOrigin(p, p_err, -ng);
    }
} ;
class SurfInteract: public Interact {
public:
    // Surface shading normal ns
	Normal ns;
    // Surface geometry coordinate system y axis ngy and ...
    Normal ngy, nsy;
    // Surface uv
    Point2 uv;
    // Surface differentials
    Vector3 dpdu, dpdv;
    Normal dndu, dndv;
    // More differentials. They are set to 0 if no RayDifferential is
    // present for calculation
    bool camera_differentials;
    Vector3 dpdx, dpdy;
    Normal dndx, dndy;
    Float dudx, dvdx, dudy, dvdy;

    // Surface attribute functions.
    BSDF * bsdf;
    BSSRDF * bssrdf;
    // Tell the compiler to do better optimization
	inline SurfInteract (): bsdf(nullptr), bssrdf(nullptr) {}
    // Construct a SurfInteract. Variables passed in are among the
    // most fundamental ones.
    inline SurfInteract (
        const Point3 & p, const Vector3 & p_err,
        const Vector3 & wh, const Normal & ng,
        const Shape * hit, const bool & fl,
        const Normal & ngy,
        const Point2 & uv,
        const Vector3 & dpdu, const Vector3 & dpdv,
        const Normal & dndu, const Normal & dndv
    ): Interact::Interact(p, p_err, wh, ng, (const Object*)hit, fl),
        ngy(ngy), uv(uv),
        dpdu(dpdu), dpdv(dpdv), 
        dndu(dndu), dndv(dndv),
        camera_differentials(false),
        dpdx(0), dpdy(0), dndx(0, 0, 0), dndy(0, 0, 0),
        dudx(0), dvdx(0), dudy(0), dvdy(0),
        bsdf(nullptr), bssrdf(nullptr) {
        assert(p_err[0] >= 0 && p_err[1] >= 0 && p_err[2] >= 0);
    }

    ~SurfInteract () {
        if(bsdf != nullptr) bfree(bsdf);
        if(bssrdf != nullptr) bfree(bssrdf);
    }

    // Continue a differential ray if null intersection is met.
    inline RayDifferential  continueRayDiff (const RayDifferential & rd, Float t) const {
        RayDifferential rt;
        rt.dxori = p+dpdx;
        rt.dyori = p+dpdy;
        rt.o = p;
        rt.dxdir = rd.dxdir;
        rt.dydir = rd.dydir;
        rt.d = rd.d;
        rt.t = t;
        return rt;
    }

    // Sample the RADIANCE emmited in wo direction.
    Spectrum Le () const ;
    // Apply material effects. Calculate the bsdf / bssrdf functions
    bool applyMaterial (bool dir) ;
    // Calculate differentials of p and uv respect to film
    // coordinates when differential rays are present.
    inline bool calculateDifferntials (const RayDifferential & rd) {
        if(!rd.differentials) {
            dpdx = dpdy = {0, 0, 0};
            dudx = dvdx = dudy = dvdy = 0;
            dndx = dndy = {0, 0, 0};
            camera_differentials = false;
            return false;
        }
        Float tx = dot(ng, p-rd.dxori)/dot(ng, rd.dxdir);
        Float ty = dot(ng, p-rd.dyori)/dot(ng, rd.dydir);
        // Not too considerate about floating point error
        if(isNan(tx)) dpdx = Vector3(Infinity);
        else dpdx = rd.dxori+rd.dxdir*tx-p;
        if(isNan(ty)) dpdy = Vector3(Infinity);
        else dpdy = rd.dyori+rd.dydir*ty-p;
        // note that px and py are always within the uv plane because cross(dpdu, dpdv) = ng
        // so there must be a solution to the equation p+Du(dpdu)+Dv(dpdv) = px/py
        // Calculate duvdxy, duvdx comes first
        Float a1 = dpdu.x, a2 = dpdu.y, a3 = dpdu.z;
        Float b1 = dpdv.x, b2 = dpdv.y, b3 = dpdv.z;
        Float cx1 = dpdx.x, cx2 = dpdx.y, cx3 = dpdx.z;
        Float cy1 = dpdy.x, cy2 = dpdy.y, cy3 = dpdy.z;
        Float d3 = a1*b2-a2*b1, d1 = a2*b3-a3*b2, d2 = a1*b3-a3*b1;
        if(abs(d3) > abs(d1) && abs(d3) > abs(d2)) {
            dudx = (cx1*b2-cx2*b1)/d3;
            dvdx = (cx2*a1-cx1*a2)/d3;
            dudy = (cy1*b2-cy2*b1)/d3;
            dvdy = (cy2*a1-cy1*a2)/d3;
        } else if(abs(d2) > abs(d1) && abs(d2) > abs(d3)) {
            dudx = (cx1*b3-cx3*b1)/d2;
            dvdx = (cx3*a1-cx1*a3)/d2;
            dudy = (cy1*b3-cy3*b1)/d2;
            dvdy = (cy3*a1-cy1*a3)/d2;
        } else {
            dudx = (cx2*b3-cx3*b2)/d1;
            dvdx = (cx3*a2-cx2*a3)/d1;
            dudy = (cy2*b3-cy3*b2)/d1;
            dvdy = (cy3*a2-cy2*a3)/d1;
        }
        // Calculate dndxy
        dndx = (Normal)(dndu*dudx+dndv*dvdx);
        dndy = (Normal)(dndu*dudy+dndv*dvdy);
        camera_differentials = true;
        return true;
    }

    // Transform world space to geometry tangent space 
    inline Vector3 toGeometryTangent (const Vector3 & d) {
        return ng.coord(ngy, d);
    }
    // Transform from world space to shading tangent space 
    inline Vector3 toShadingTangent (const Vector3 & d) {
        return ns.coord(nsy, d);
    }// Transform to world space from geometry tangent space 
    inline Vector3 fromGeometryTangent (const Vector3 & d) {
        return ng.rcoord(ngy, d);
    }
    // Transform to world space from shading tangent space 
    inline Vector3 fromShadingTangent (const Vector3 & d) {
        return ns.rcoord(ngy, d);
    }

};

}

#endif